js-05 函数

一、函数的作用

  1. 主要用来储存一段代码,

  2. 代码重复使用,大量减少代码量

二、函数声明及调用

  1. 普通声明:function name(){content}
  2. 表达式声明: var fn = function(){countent}
  3. 函数调用:name()
  4. 函数的length属性,为形参的个数;

函数注意事项

  • 函数调用时,是不分先后的;

  • 在任何地方给一个已经声明且赋值的变量再重新声明,重新声明都是无效的。

  • 函数重复声明时,后面的函数会覆盖前面的函数;

  • 如果先用函数表达式声明的,后面声明的普通函数是不能覆盖前面的函数,如果前面是普通声明,后面是表达式声明,则照常执行;

var x = function () { alert(1) };
x();    //1
function x() { alert(3) };
x();     //1


function x() { alert(3) };
x();     //3
var x = function () { alert(1) };
x();    //1

面试题:

var x = 1;
var y = 0;
var z = 0;
function add(n){return n=n+1;}
y = add(x);
function add(n){return n=n+3;}
z = add(x);
s=y+z;

y和z都是4,后面定义的影响到前面定义的;

函数放在其他位置

  1. 对象中的函数时

    var aa = {name:"张三",fn:function(){循环体}}
    aa.fn();
    

    经常使用于函数过多,变量名重复;

  2. 数组中的函数时

    var ary = [1,2,function(){}] 
    // 调用:ary[3]()
    

三、全局函数

JavaScript 中包含以下 7 个全局函数,用于完成一些常用的功能:

escape( )eval( )isFinite( )isNaN( )parseFloat( )parseInt( )unescape( )

注意:setTimeout 不是js的全局函数,是window的全局函数

四、封装函数

  1. 写出主体代码;

  2. 找出可变的变量,用形参代替;

  3. 放到函数中;

  4. 调用函数并检查;

五、事件处理函数

obox.onclick = aa;

事件处理函数当中,函数后面不需要跟小括号;

六、函数参数的类型:

  1. 形参和实参

    • 创建:function 函数名(形参){} 多个形参用逗号隔开;

    • 调用:函数名(实参);

    注意:

    • 形参比实参多,多出来的形参值为undefined;

    • 实参比形参多,多出来的实参不参与计算,等于没有被识别;

  2. arguments

    它是一个对象,是一个类数组(有长度,可以循环,不能使用数组的方法);

    类似于数组,数据由实参决定,arguments是函数的内置对象;

    当使用一个函数不知道这个函数可以使用哪些形参时,可以利用arguments获取可以使用的形参;可以使用下标方式取值,比如:arguments[0];

    //查询map函数有哪些形参可用;
    ary.map(function(){
        console.log(arguments)
    })
    

七、回调函数

  1. 把函数作为实参传递到另一个函数当中,为回调函数;

  2. 回调函数是某个操作或某个动作做完以后调用的函数。

回调函数实例:

function add(a,b){
    return a + b;
}
function fn(num1,num2,func){   //这里fn用来传入一个函数;
    return func(num1,num2);
}
console.log(fn(10,20,add)); //这里不能使用小括号,因为在fn函数里面已经调用小括号了;

八、递归函数

  1. 在函数中自己调用自己,也可以是一个for循环;

  2. 递归一定要有结束条件;

递归: 斐波那契数列第几个数是什么;

function fn(n){
	if(n===1 || n===2) return 1;        必须return 1个数值
	return fn(n-1) + fn(n - 2);
}


// 求n个数字的和;   递归函数自己调用自己;
function fn(n){
    if(n == 1) return 1;
    return n + fn(n-1)
}
console.log(3);
// 3 + fn(2)
// 3 + (2 + fn(1))
// 3 + (2 + (1))

尾递归

  1. 什么是尾调用:指某个函数的最后一步是调用另一个函数

    // 这种情况叫尾调用
    function f(x){
        return g(x)
    }
    
    // 这种情况不叫尾调用
    function f(x){
        let y = g(x)
        return y
        // 调用函数g后还有别的操作,不叫尾调用;
    }
    // 情况二 也不叫尾调用
    function f(x){
        return g(x)+1  //调用后还有操作;
    }
    
  2. 尾递归:函数调用自身,称为递归,如果尾调用自身,称为尾递归

    递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生栈溢出错误,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生栈溢出错误;

    递归保存n个调用记录,时间复杂度:O(n)

    尾递归只保留一个调用记录,时间复杂度:O(1)

    // 递归写法
    function fabonacci(n){
        if(n===1 || n===2) return 1
        return fabonacci(n-1) + fabonacci(n-2) 
         //一直在进行记录,数值过大栈溢出;
    }
    
    // 尾递归写法;
    function fabonacci(n,start=1,total=1){
        if(n===1 || n===2) return total
        return fabonacci(n-1, total, start+total) 
    }
    /**
     * 尾递归斐波那契分析:
     * 第一个为次数,第二个为保留上一次的结果,第三个用于返回的结果;
     * 假如传入10,第一次为 9 1 2
     * 第二次为  8 2 3
     * 第三次为  7 3 5
     * 最后一位依次为:  2  3  5  8
     * 在es6中,只要使用尾递归,就不会发生栈溢出,相对节省内存;
     */
    
    function fib(n) {
        let p = 0, c = 1;
        while(n--) 
          [p, c] = [c, p + c]
        return p;
    }
    

九、作用域

每个函数都有自己的执行期上下文,每个执行期上下文都有自己的变量对象;

全局变量的作用域

  1. 用var声明的变量,可以在全局使用;

  2. 除了在函数内部定义的变量都是全局变量;

  3. 全局作用域声明的变量、函数会变成window的方法或属性。

  4. 全局变量如果页面不关闭,全局变量就不会释放,就会占空间,消耗内存,所以尽可能的在函数里面使用局部变量;

局部变量

在函数内部定义的变量是局部变量,

块级作用域

花括号里面声明的变量只能在花括号当中使用,比如:if语句和for循环语句;

但是在js当中,外面也能使用花括号的变量,因为在js里没有块级作用域,只有函数除外,在es6里面let声明的变量有作用域;

隐式全局变量

  1. 声明变量没有var,就叫隐式全局变量;

  2. 如果在函数里面声明变量没有var,函数执行之后,外面也能拿到函数里面的变量;

注意:

  • 全局变量是不能被删除的,隐式全局变量时可以被删除的;

  • 定义变量使用var是不能被删除的,没有var是可以删除的;

delete 变量 删除变量。

[[scope]]

(这里跳过,建议去看视频,推荐渡一教育的视频,作用域讲的比较详细)

作用域是因为由函数的产生而产生的作用域;

每个javascript函数也是一个对象,函数叫类对象,函数可以访问它的name属性和prototype属性,但有些属性是不能访问的,比如[[scope]],这些属性仅javascript引擎存取;

AO(Active Object)是执行期上下文,记录了该环境中所有声明的参数,变量和函数;

GO(Global Object)是全局产生的执行期上下文;

全称:VO(variable),AO是正在执行的VO,GO全局中的VO;

[[scope]]是隐式的属性,不能拿出来,我们不能访问,系统可以用,里面存储了执行期上下文(AO)的集合。

执行期上下文:

当一个函数执行时,会创建一个称为执行期上下文(AO)的内部对象,

一个执行期上下文定义了一个函数执行时的环境,

函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,

当函数执行完毕,它所产生的执行上下文被销毁;

查找变量,从作用域链的顶端依次向下查找;

作用域链:[[scope]]中存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫作用域链;

每一个函数都有一个执行期上下文的集合,叫作用域链;

当一个函数被创建时,就可以访问fn.namefn.[[scope]]

fn.[[scope]]里面存放了一个0,0指向GO,global Object;

一个函数被定义时,可以访问[[scope]],里面储存的是GO,一个函数执行之后会产生一个新的执行期上下文,存的是AO,GO被压下去了,变成了第1位,AO在第0 位,在最顶端;

当函数里面还有函数时,子函数可以访问父函数的劳动成果,可以访问父函数的[[scope]],里面还存有自己的执行期上下文(AO);

查找一个变量,scope会从顶端开始查找作用域链,如果顶端查找不到就会查找下一个;

函数在被调用执行时,会创建一个当前函数的执行期上下文,在该执行期上下文的创建阶段,变量对象、作用域、闭包、this指向会分别被确定,而一个JavaScript程序中一般来说会有多个函数,JavaScript引擎使用调用栈来管理这些函数的调用顺序,函数调用栈的调用顺序与栈数据结构一致;

例子:

a定义之后会产生一个scope,scope里面储存的是全局的执行上下文GO,之后a被执行,产生了一个AO,被放到了scope的最顶端,形成了一个新的作用域链,

function a(){
    function b(){
        var b = 234;
    }
    var a = 123
    b();
}
var glob = 100
a()

image

javascript变量的生命周期

JavaScript 变量生命周期在它声明时初始化。

局部变量在函数执行完毕后销毁。

全局变量在页面关闭后销毁。

十、作用域链:

从内到外查找,先找自己局部区域有没有,再往外一层

作用域链:[[scope]]中所存储的执行期上下文(AO)对象的集合,这个集合呈链式链接,我们把这种链式链接叫作用域链;

十一、预解析(变量提升)

  1. 执行代码前,会先找页面中的(var 和 function),找出来之后并把变量赋值为undefined,函数永远是最后提升的

  2. 逐行解析:遇到赋值表达式时,会将值重新赋值给变量;

  3. 变量的提升:只会在当前的作用域中提升,提升到当前的作用域的最上面; 函数中的变量只会提前到函数的作用域最前面,不会提升到外面去;

作用域预解析必背四个步骤

  • 创建AO/GO对象,

  • 变量提升并赋值undefined

  • 形参实参相统一;

  • 函数永远是最后提升的,函数最后提升到最上面;

// 由于函数最后提升,先提升var,赋值undefined,之后提升function,a被赋值函数,因此第一个打印函数;
// 之后a又被赋值,因此之后的值一直是a的,function已经被提升到最上面了;
console.log(a);
function a(){
    console.log("aaaa");
}
var a = 1;
function a(){
    console.log('bbbb')
}
console.log(a);

十二、this

this定义

  • 函数中的this指向,不是在定义的时候确定的,而是在调用的时候确定的;

  • 当事件放到循环里面时,是拿不到i的,需要用this代替这个,比如:

for(var i=0;i<oli.length;i++){
    oli[i].onmouseover = function(){
        this.style.display = "block";    //事件里面的i是无法获取到会报错的;这个this代表划过的这个;
}
  • this可以在js任何地方使用,位置不同指向不同,

  • 全局的任何地方都可以调用,window是页面中最大的对象,

this指向

(1)触发当前事件的对象;

(2)普通函数的this指向window;

(3)构造函数的this指向实例对象;

(4)方法中的this指向实例对象;

(5)原型中的this指向实例对象

(6)定时器中的this指向window;

(7)函数作为某个对象的方法时,this指向这个对象;

(8)函数作为某个数组的数据时,this指向这个数组;

总结:执行代码前,会把变量的声明提前,变量值为undefined,当遇到变量值时,并给变量赋值;

面试题2:

fn();
console.log(c);
console.log(b);
console.log(a);
function fn(){
    var a=b=c=1;
    console.log(c);
    console.log(b);
    console.log(a);
}

解答: 执行完是这样的:

function fn(){
    var a=undefined;
    a = 1;
    b=1;
    c=1;
}     //只有b和c没有var,是隐式全局变量,所以外面可以拿到值,a被声明了是局部变量,所以a报错了;

相同题:

var a,b
(function(){
    alert(a);
    alert(b);
    var a=b=3;
    alert(a);
    alert(b);
}());
alert(a);
alert(b);

面试题3:

f1();
var f1=function(){
console.log(a);
var a = 10;}

解答:

var f1;
f1();
f1=function(){};    //var声明的函数,函数为表达式,是赋值的,变量名会提前,;f1找不到;

面试题4:

var color = 'green';
var test4399 = {
    color:'blue',
    getColor:function(){
        var color = 'red';
        alert(this.color);
    }
}
var getColor = test4399.getColor;
getColor();
test4399.getColor();

解答:

第一个执行结果是:window.getColor() this指向window,是green;

第二个执行结果:test4399.getColor();this指向test4399,是blue;

十三、函数返回值

  1. 函数调用以后的结果,就是return后面的值;
  2. 如果没有return,则返回undefined;
  3. return 后面的代码不会再执行;
  4. return后面有什么,最终返回结果就是什么;
  5. 直接调用函数,需要使用一个变量接收一下函数;
  6. 如果函数return后面没有任何值,返回结果将会是undefined;

总结:使用return就必须有返回值,要么就永远不使用;

return 返回多个值时,只能返回最后一个值,如果需要返回多个值可以放到一个数组当中,或对象中返回;

返回值的类型可以是js中数据类型中的任何一个;

面试题:

var x = 1, y = z = 0;
function add(n) {
    n = n + 1;
};
y = add(x);
function add(n) {
    n = n + 3;
};
z = add(x);
console.log(x,y,z);

解答: 两个函数都没有return 返回值,所有x和y都为undefined;

十四、函数注释

image

实例:

/**
*
*@param num {Number} 数字
*@param str {String} 名称
*@return {Boolean} true:成年 false:未成年
* @type { import('vite').UserConfig } // 使用ts类型
*/
function check(num,str){
    if(num === 18) {
        return true
    }
}

其他函数调用方式

function fn(one, two, three){
    console.log(one, two, three)
}
const a = 'hh'
const b = 'ss'
fn`this a is ${a} and b is ${b}`

使用模板字符串调用函数时:

  • 第一个参数:数组,包含模板字符串分割的字符串
  • 第二个参数:模板字符串的第一个变量,后面的变量以此类推

高频面试题

● 写一个获取非行间样式的函数

● 说说你对作用域链的理解?

● var x = 1, y = z = 0; function add(n) { n = n + 1; }; y = add(x); function add(n) { n = n + 3; }; z = add(x); console.log(x,y,z);

● 请解释变量提升?

● 函数声明和函数表达式声明的区别?

● JavaScript 两种变量范围有什么不同?